﻿/*
    MAST -- A file Merging and Splitting Tool
    Copyright (C) 2011 Harry Rose

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace FileMergeAndSplitTool
{
    /// <summary>
    ///     A delegate that is used to report the progress of a split operation
    /// </summary>
    /// <param name="splittableFile">The file that is being split</param>
    /// <param name="currentPart">The name of the current split file that is being written to</param>
    /// <param name="percentComplete">What percentage of the file has been split</param>
    public delegate void SplitProgress(SplittableFile splittableFile, string currentPart, double percentComplete);

    /// <summary>
    ///     A class that represents a file on the filesystem that is to be split into multiple, smaller files.
    /// </summary>
    public class SplittableFile : MAST_File
    {
        public event SplitProgress ProgressUpdate;
        public const string PartToken = "%part%";

        #region PrivateInstanceVariables
        
        const int bufferSizeBytes = 1024 * 1024 * 10;
        string originalFilename = "";
        int splitFilesizeMB = 100;

        

        private string filenameTemplate;
        private object filenameTemplateLock = new object();
        private string outputDirectory;
        List<FilePart> parts;
        object partsLock = new object();

        #endregion

        #region Properties

        /// <summary>
        ///     The directory in which the split files will be stored
        /// </summary>
        public string OutputDirectory
        {
            get { return outputDirectory; }
        }
        
        /// <summary>
        ///     The template used for naming the files.  It should contain the value of PartToken at least once, otherwise it will be appended to the end.
        /// </summary>
        public string FilenameTemplate
        {
            get { return filenameTemplate; }
            set
            {
                lock (filenameTemplateLock)
                {
                    if (!value.Contains(PartToken))
                    {
                        value += PartToken;
                    }

                    filenameTemplate = value;
                }
            }
        }

        /// <summary>
        ///     A list of the parts that this file will be split into.  The parts' appearance in this list is not an indication of their existence on the filesystem.
        /// </summary>
        public List<FilePart> Parts
        {
            get {
                lock (partsLock)
                {
                    return parts;
                }
            }
        }

        /// <summary>
        ///     The maximum size in megabytes that each split file should be.
        /// </summary>
        public int SplitSize
        {
            get { return splitFilesizeMB; }
            set
            {
                lock (partsLock)
                {
                    lock (filenameTemplateLock)
                    {
                        int totalParts = (int)Math.Ceiling(FileInfo.Length / (double)MBtoB(value));
                        parts.Clear();
                        for (int i = 0; i < totalParts; i++)
                        {
                            parts.Add(new FilePart(GetFilePartName(i, totalParts), i));
                        }
                        splitFilesizeMB = value;
                    }
                }
            }
        }

        #endregion

        #region Constructors

        /// <summary>
        ///     Create a SplittableFile
        /// </summary>
        /// <param name="filename">The filename (and path) of the file to be split</param>
        /// <param name="outputDirectory">The directory that the split files will be stored into</param>
        /// <param name="splitFilenameTemplate">The pattern that will be used in naming the output files. <see cref="FilenameTemplate"/> </param>
        public SplittableFile(string filename, string outputDirectory, string splitFilenameTemplate)
            :base(filename)
        {
            parts = new List<FilePart>();
            originalFilename = filename;
            this.outputDirectory = outputDirectory;
            this.FilenameTemplate = splitFilenameTemplate;
        }

        /// <summary>
        ///     Create a SplittableFile
        /// </summary>
        /// <param name="filename">The filename (and path) of the file to be split</param>
        /// <param name="outputDirectory">The directory that the split files will be stored into</param>
        /// <param name="splitFilenameTemplate">The pattern that will be used in naming the output files. <see cref="FilenameTemplate"/> </param>
        /// <param name="splitSize">The maximum size in megabytes that each split file should be.</param>
        public SplittableFile(string filename, string outputDirectory, string splitFilenameTemplate, int splitSize)
            :this(filename,outputDirectory,splitFilenameTemplate)
        {
            if (splitSize < 1)
            {
                throw new ArgumentOutOfRangeException("splitSize", "splitSize must be greater than 0MB");
            }
            SplitSize = splitSize;
        }

        #endregion

        #region PublicMethods

        /// <summary>
        ///     Split the file into multiple, smaller files.
        /// </summary>
        public void Split()
        {
            BinaryReader reader = new BinaryReader(new FileStream(originalFilename, FileMode.Open));

            byte[] buffer = new byte[bufferSizeBytes];

            int bytesRead = 0;
            int totalBytesRead = 0;
            int partNo = 0;
            int noParts = (int)Math.Ceiling(FileInfo.Length / (double) MBtoB(splitFilesizeMB));

            int maxBytesPerPart = (int) MBtoB(splitFilesizeMB);

            while (totalBytesRead < FileInfo.Length)
            {
                partNo++;
                string currentFilename = GetFilePartName(partNo,noParts);
                BinaryWriter writer = new BinaryWriter(new FileStream(currentFilename,FileMode.Create));

                int bytesWrittenToPart = 0;
                int bytesToGo = maxBytesPerPart;
                do
                {
                    bytesRead = reader.Read(buffer, 0, ( bytesToGo > bufferSizeBytes) ? bufferSizeBytes : bytesToGo);

                    bytesWrittenToPart += bytesRead;
                    bytesToGo -= bytesRead;
                    totalBytesRead += bytesRead;
                    writer.Write(buffer, 0, bytesRead);

                    ProgressUpdate(this, currentFilename, 100 * (totalBytesRead / (double)FileInfo.Length));
                }
                while (bytesRead == bufferSizeBytes && totalBytesRead < FileInfo.Length);

                writer.Close();
            }

            reader.Close();
        }

        #endregion

        #region PrivateMethods
        private string GetFilePartName(int part, int totalParts)
        {
            int noDigits = (int)Math.Ceiling(Math.Log10(totalParts));
            return Path.Combine(outputDirectory, filenameTemplate.Replace(PartToken, ("" + part).PadLeft(noDigits, '0')));
        }

        private long MBtoB(int MB)
        {
            return MB * 1024 * 1024;
        }
        #endregion
    }
}
